{
  config,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.services.radicle;

  json = pkgs.formats.json { };

  env = rec {
    # rad fails if it cannot stat $HOME/.gitconfig
    HOME = "/var/lib/radicle";
    RAD_HOME = HOME;
  };

  # Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
  rad-system = pkgs.writeShellScriptBin "rad-system" ''
    set -o allexport
    ${lib.toShellVars env}
    # Note that --env is not used to preserve host's envvars like $TERM
    exec ${lib.getExe' pkgs.util-linux "nsenter"} -a \
      -t "$(${lib.getExe' config.systemd.package "systemctl"} show -P MainPID radicle-node.service)" \
      -S "$(${lib.getExe' config.systemd.package "systemctl"} show -P UID radicle-node.service)" \
      -G "$(${lib.getExe' config.systemd.package "systemctl"} show -P GID radicle-node.service)" \
      ${lib.getExe' cfg.package "rad"} "$@"
  '';

  commonServiceConfig = serviceName: {
    environment = env // {
      RUST_LOG = lib.mkDefault "info";
    };
    path = [
      pkgs.gitMinimal
    ];
    documentation = [
      "https://docs.radicle.xyz/guides/seeder"
    ];
    after = [
      "network.target"
      "network-online.target"
    ];
    requires = [
      "network-online.target"
    ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = lib.mkMerge [
      {
        BindReadOnlyPaths = [
          "${cfg.configFile}:${env.RAD_HOME}/config.json"
          "${
            if lib.types.path.check cfg.publicKey then
              cfg.publicKey
            else
              pkgs.writeText "radicle.pub" cfg.publicKey
          }:${env.RAD_HOME}/keys/radicle.pub"
          "${config.security.pki.caBundle}:/etc/ssl/certs/ca-certificates.crt"
        ];
        KillMode = "process";
        StateDirectory = [ "radicle" ];
        User = config.users.users.radicle.name;
        Group = config.users.groups.radicle.name;
        WorkingDirectory = env.HOME;
      }
      # The following options are only for optimizing:
      # systemd-analyze security ${serviceName}
      {
        BindReadOnlyPaths = [
          "-/etc/resolv.conf"
          "/run/systemd"
        ];
        AmbientCapabilities = "";
        CapabilityBoundingSet = "";
        DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
        LockPersonality = true;
        MemoryDenyWriteExecute = true;
        NoNewPrivileges = true;
        PrivateTmp = true;
        ProcSubset = "pid";
        ProtectClock = true;
        ProtectHome = true;
        ProtectHostname = true;
        ProtectKernelLogs = true;
        ProtectProc = "invisible";
        ProtectSystem = "strict";
        RemoveIPC = true;
        RestrictAddressFamilies = [
          "AF_UNIX"
          "AF_INET"
          "AF_INET6"
        ];
        RestrictNamespaces = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        RuntimeDirectoryMode = "700";
        SocketBindDeny = [ "any" ];
        StateDirectoryMode = "0750";
        SystemCallFilter = [
          "@system-service"
          "~@aio"
          "~@chown"
          "~@keyring"
          "~@memlock"
          "~@privileged"
          "~@resources"
          "~@setuid"
          "~@timer"
        ];
        SystemCallArchitectures = "native";
        # This is for BindPaths= and BindReadOnlyPaths=
        # to allow traversal of directories they create inside RootDirectory=
        UMask = "0066";
      }
    ];
    confinement = {
      enable = true;
      mode = "full-apivfs";
      packages = [
        pkgs.gitMinimal
        cfg.package
        pkgs.iana-etc
        (lib.getLib pkgs.nss)
        pkgs.tzdata
      ];
    };
  };
in
{
  options = {
    services.radicle = {
      enable = lib.mkEnableOption "Radicle Seed Node";
      package = lib.mkPackageOption pkgs "radicle-node" { };
      privateKeyFile = lib.mkOption {
        # Note that a key encrypted by systemd-creds is not a path but a str.
        type = with lib.types; either path str;
        description = ''
          Absolute file path to an SSH private key,
          usually generated by `rad auth`.

          If it contains a colon (`:`) the string before the colon
          is taken as the credential name
          and the string after as a path encrypted with `systemd-creds`.
        '';
      };
      publicKey = lib.mkOption {
        type = with lib.types; either path str;
        description = ''
          An SSH public key (as an absolute file path or directly as a string),
          usually generated by `rad auth`.
        '';
      };
      node = {
        listenAddress = lib.mkOption {
          type = lib.types.str;
          default = "[::]";
          example = "127.0.0.1";
          description = "The IP address on which `radicle-node` listens.";
        };
        listenPort = lib.mkOption {
          type = lib.types.port;
          default = 8776;
          description = "The port on which `radicle-node` listens.";
        };
        openFirewall = lib.mkEnableOption "opening the firewall for `radicle-node`";
        extraArgs = lib.mkOption {
          type = with lib.types; listOf str;
          default = [ ];
          description = "Extra arguments for `radicle-node`";
        };
      };
      configFile = lib.mkOption {
        type = lib.types.package;
        internal = true;
        default = (json.generate "config.json" cfg.settings).overrideAttrs (previousAttrs: {
          preferLocalBuild = true;
          # None of the usual phases are run here because runCommandWith uses buildCommand,
          # so just append to buildCommand what would usually be a checkPhase.
          buildCommand =
            previousAttrs.buildCommand
            + lib.optionalString cfg.checkConfig ''
              ln -s $out config.json
              install -D -m 644 /dev/stdin keys/radicle.pub <<<"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgFMhajUng+Rjj/sCFXI9PzG8BQjru2n7JgUVF1Kbv5 snakeoil"
              export RAD_HOME=$PWD
              ${lib.getExe' pkgs.buildPackages.radicle-node "rad"} config >/dev/null || {
                cat -n config.json
                echo "Invalid config.json according to rad."
                echo "Please double-check your services.radicle.settings (producing the config.json above),"
                echo "some settings may be missing or have the wrong type."
                exit 1
              } >&2
            '';
        });
      };
      checkConfig =
        lib.mkEnableOption "checking the {file}`config.json` file resulting from {option}`services.radicle.settings`"
        // {
          default = true;
        };
      settings = lib.mkOption {
        description = ''
          See <https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/radicle/src/node/config.rs#L275>
        '';
        default = { };
        example = lib.literalExpression ''
          {
            web.pinned.repositories = [
              "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" # heartwood
              "rad:z3trNYnLWS11cJWC6BbxDs5niGo82" # rips
            ];
          }
        '';
        type = lib.types.submodule {
          freeformType = json.type;
        };
      };
      httpd = {
        enable = lib.mkEnableOption "Radicle HTTP gateway to radicle-node";
        package = lib.mkPackageOption pkgs "radicle-httpd" { };
        listenAddress = lib.mkOption {
          type = lib.types.str;
          default = "127.0.0.1";
          description = "The IP address on which `radicle-httpd` listens.";
        };
        listenPort = lib.mkOption {
          type = lib.types.port;
          default = 8080;
          description = "The port on which `radicle-httpd` listens.";
        };
        aliases = lib.mkOption {
          type = lib.types.attrsOf lib.types.str;
          description = "Alias and RID pairs to shorten git clone commands for repositories.";
          default = { };
          example = lib.literalExpression ''
            {
              heartwood = "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5";
            }
          '';
        };
        nginx = lib.mkOption {
          # Type of a single virtual host, or null.
          type = lib.types.nullOr (
            lib.types.submodule (
              lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
                options.serverName = {
                  default = "radicle-${config.networking.hostName}.${config.networking.domain}";
                  defaultText = "radicle-\${config.networking.hostName}.\${config.networking.domain}";
                };
              }
            )
          );
          default = null;
          example = lib.literalExpression ''
            {
              serverAliases = [
                "seed.''${config.networking.domain}"
              ];
              enableACME = false;
              useACMEHost = config.networking.domain;
            }
          '';
          description = ''
            With this option, you can customize an nginx virtual host which already has sensible defaults for `radicle-httpd`.
            Set to `{}` if you do not need any customization to the virtual host.
            If enabled, then by default, the {option}`serverName` is
            `radicle-''${config.networking.hostName}.''${config.networking.domain}`,
            TLS is active, and certificates are acquired via ACME.
            If this is set to null (the default), no nginx virtual host will be configured.
          '';
        };
        extraArgs = lib.mkOption {
          type = with lib.types; listOf str;
          default = [ ];
          description = "Extra arguments for `radicle-httpd`";
        };
      };
    };
  };

  config = lib.mkIf cfg.enable (
    lib.mkMerge [
      {
        systemd.services.radicle-node = lib.mkMerge [
          (commonServiceConfig "radicle-node")
          {
            description = "Radicle Node";
            documentation = [ "man:radicle-node(1)" ];
            serviceConfig = {
              ExecStart = "${lib.getExe' cfg.package "radicle-node"} --force --listen ${cfg.node.listenAddress}:${toString cfg.node.listenPort} ${lib.escapeShellArgs cfg.node.extraArgs}";
              Restart = lib.mkDefault "on-failure";
              RestartSec = "30";
              SocketBindAllow = [ "tcp:${toString cfg.node.listenPort}" ];
              SystemCallFilter = lib.mkAfter [
                # Needed by git upload-pack which calls alarm() and setitimer() when providing a rad clone
                "@timer"
              ];
            };
            confinement.packages = [
              cfg.package
            ];
          }
          # Give only access to the private key to radicle-node.
          {
            serviceConfig =
              let
                keyCred = builtins.split ":" "${cfg.privateKeyFile}";
              in
              if lib.length keyCred > 1 then
                {
                  LoadCredentialEncrypted = [ cfg.privateKeyFile ];
                  # Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
                  BindReadOnlyPaths = [
                    "/run/credentials/radicle-node.service/${lib.head keyCred}:${env.RAD_HOME}/keys/radicle"
                  ];
                }
              else
                {
                  LoadCredential = [ "radicle:${cfg.privateKeyFile}" ];
                  BindReadOnlyPaths = [
                    "/run/credentials/radicle-node.service/radicle:${env.RAD_HOME}/keys/radicle"
                  ];
                };
          }
        ];

        environment.systemPackages = [
          rad-system
        ];

        networking.firewall = lib.mkIf cfg.node.openFirewall {
          allowedTCPPorts = [ cfg.node.listenPort ];
        };

        users = {
          users.radicle = {
            description = "Radicle";
            group = "radicle";
            home = env.HOME;
            isSystemUser = true;
          };
          groups.radicle = {
          };
        };
      }

      (lib.mkIf cfg.httpd.enable (
        lib.mkMerge [
          {
            systemd.services.radicle-httpd = lib.mkMerge [
              (commonServiceConfig "radicle-httpd")
              {
                description = "Radicle HTTP gateway to radicle-node";
                documentation = [ "man:radicle-httpd(1)" ];
                serviceConfig = {
                  ExecStart = lib.escapeShellArgs (
                    [
                      (lib.getExe' cfg.httpd.package "radicle-httpd")
                      "--listen=${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}"
                    ]
                    ++ lib.flatten (
                      lib.mapAttrsToList (alias: rid: [
                        "--alias"
                        alias
                        rid
                      ]) cfg.httpd.aliases
                    )
                    ++ cfg.httpd.extraArgs
                  );
                  Restart = lib.mkDefault "on-failure";
                  RestartSec = "10";
                  SocketBindAllow = [ "tcp:${toString cfg.httpd.listenPort}" ];
                  SystemCallFilter = lib.mkAfter [
                    # Needed by git upload-pack which calls alarm() and setitimer() when providing a git clone
                    "@timer"
                  ];
                };
                confinement.packages = [
                  cfg.httpd.package
                ];
              }
            ];
          }

          (lib.mkIf (cfg.httpd.nginx != null) {
            services.nginx.virtualHosts.${cfg.httpd.nginx.serverName} = lib.mkMerge [
              cfg.httpd.nginx
              {
                forceSSL = lib.mkDefault true;
                enableACME = lib.mkDefault true;
                locations."/" = {
                  proxyPass = "http://${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}";
                  recommendedProxySettings = true;
                };
              }
            ];

            services.radicle.settings = {
              node.alias = lib.mkDefault cfg.httpd.nginx.serverName;
              node.externalAddresses = lib.mkDefault [
                "${cfg.httpd.nginx.serverName}:${toString cfg.node.listenPort}"
              ];
            };
          })
        ]
      ))
    ]
  );

  meta.maintainers = with lib.maintainers; [
    julm
    lorenzleutgeb
  ];
}
